package consul

import (
	"fmt"
	"github.com/google/uuid"
	"github.com/hashicorp/consul/api"
	"github.com/pkg/errors"
	"go.uber.org/zap"
	"gobase/logger"
	"strconv"
	"strings"
)

type consulClient struct {
	client api.Client
}

type consulLock struct {
	lock *api.Lock
}

// NewConsul new consul client
func NewConsul(config *Config) (c Client, err error) {
	var client *api.Client
	if config == nil {
		client, err = api.NewClient(api.DefaultConfig())
	} else {
		client, err = api.NewClient(&api.Config{
			Address: config.Address,
		})
	}

	if err != nil {
		return nil, err
	}

	return &consulClient{
		client: *client,
	}, nil
}

// Register implementation register client
func (c *consulClient) Register(options Options) (string, error) {
	registration, err := newRegistration(options)
	if err != nil {
		return "", errors.Wrapf(err, "newRegistration error with options %q", options)
	}

	err = c.client.Agent().ServiceRegister(registration)
	if err != nil {
		return "", errors.Wrapf(err, "service register error with options %q", options)
	}

	return registration.ID, nil
}

// UnRegister ServiceUnRegister unregister service
func (c *consulClient) UnRegister(options Options) (err error) {
	logger.Debug("Service UnRegister ", zap.String("name", options.ServiceName))
	err = c.client.Agent().ServiceDeregister(options.ServiceName + "-" + options.Address + ":" + strconv.Itoa(options.ServicePort) + "-" + options.Type.String())
	return
}

// Discover discover service with prefix service name
func (c *consulClient) Discover(params DisconverParams) ([]*ServiceInfo, error) {
	var serviceInfos []*ServiceInfo
	services, _, err := c.client.Catalog().Services(&api.QueryOptions{})
	if err != nil {
		return nil, errors.Wrapf(err, "discovery error with prefix %s", params.Prefix)
	}

	for name := range services {
		if params.Suffix {
			if !strings.HasSuffix(name, params.Prefix) {
				continue
			}
		} else {
			if !strings.HasPrefix(name, params.Prefix) {
				continue
			}
		}
		servicesData, _, err := c.client.Health().Service(name, params.Tag, true, &api.QueryOptions{})
		if err != nil {
			logger.Error("get health service with service name ", zap.String("name", name), zap.Error(err))
			continue
		}

		if len(servicesData) == 0 {
			logger.Info("get service zero with prefix", zap.String("prefix", params.Prefix), zap.String("name", name))
		}
		for _, entry := range servicesData {

			if color, ok := entry.Service.Meta["color"]; ok && color != params.Color {
				logger.Debug("skip", zap.String("service", entry.Service.Service), zap.String("color", color), zap.String("want", params.Color))
				continue
			}
			for _, health := range entry.Checks {
				if health.ServiceName != name {
					continue
				}
				addr := entry.Service.Address + ":" + strconv.Itoa(entry.Service.Port)
				serviceName := health.ServiceName
				serviceID := health.ServiceID
				serviceInfos = append(serviceInfos, &ServiceInfo{
					Addr:        addr,
					ServiceName: serviceName,
					ServiceID:   serviceID,
					Meta:        entry.Service.Meta,
					Tags:        entry.Service.Tags,
				})
			}
		}
	}
	//logger.Debug("discover services success", zap.Any("prefix", params.Prefix), zap.Any("data", serviceInfos))
	return serviceInfos, nil
}

// Store 存储key value
func (c *consulClient) Store(key string, value string) error {
	kv := &api.KVPair{
		Key:   key,
		Flags: 0,
		Value: []byte(value),
	}
	_, err := c.client.KV().Put(kv, nil)
	if err != nil {
		logger.Error("StoreKeyValue failed", zap.Error(err))
		return err
	}
	logger.Debug("store data", zap.String("key", kv.Key), zap.String("value:", value))
	return nil
}

// Load 查询key，存在返回value和bool类型值true
// 否则返回""和bool类型值false
func (c *consulClient) Load(key string) (string, bool) {
	kv, _, err := c.client.KV().Get(key, nil)
	if err != nil {
		logger.Error("load value failed", zap.String("key", key), zap.Error(err))
		return "", false
	}
	if kv == nil {
		return "", false
	}
	return string(kv.Value), true
}

func (c *consulClient) Delete(key string) error {
	_, err := c.client.KV().Delete(key, nil)
	if err != nil {
		logger.Error("delete key failed", zap.String("key", key), zap.Error(err))
		return err
	}
	return nil
}

// NewLock 新建锁
func (c *consulClient) NewLock(key string) (Locker, error) {
	l, err := c.client.LockOpts(&api.LockOptions{
		Key:         key,
		SessionName: uuid.New().String(),
	})
	if err != nil {
		return nil, err
	}
	return &consulLock{
		lock: l,
	}, nil
}

// Lock .
func (l *consulLock) Lock(stopCh <-chan struct{}) error {
	c, err := l.lock.Lock(stopCh)
	select {
	case _, isOpen := <-c:
		if !isOpen {
			return errors.Wrapf(errors.New("session invalidation channel is close now"), "")
		}
	default:
		return err
	}
	return nil
}

// UnLock .
func (l *consulLock) UnLock() error {
	return l.lock.Unlock()
}

func newCheck(registration api.AgentServiceRegistration, options Options) (*api.AgentServiceCheck, error) {
	check := new(api.AgentServiceCheck)
	if options.Type.IsRPC() {
		check.GRPC = fmt.Sprintf("%s:%d%s", registration.Address, options.HealthPort, options.HealthMethod)
	} else if options.Type.IsHTTP() {
		check.HTTP = fmt.Sprintf("http://%s:%d%s", registration.Address, options.HealthPort, options.HealthMethod)
	} else if options.Type.IsHTTPS() {
		check.TLSSkipVerify = true
		check.HTTP = fmt.Sprintf("https://%s:%d%s", registration.Address, options.HealthPort, options.HealthMethod)
	} else {
		return nil, errors.Wrapf(fmt.Errorf("newCheck error with address %s type %d", registration.Address, options.Type), "")
	}
	check.Interval = "10s"
	check.Timeout = "5s"

	if options.Interval != "" {
		check.Interval = options.Interval
	}
	if options.Timeout != "" {
		check.Timeout = options.Timeout
	}

	return check, nil
}

func newRegistration(options Options) (*api.AgentServiceRegistration, error) {
	registration := new(api.AgentServiceRegistration)

	registration.ID = options.ServiceName + "-" + options.Address + ":" + strconv.Itoa(options.ServicePort) + "-" + options.Type.String()
	registration.Name = options.ServiceName
	registration.Port = options.ServicePort
	registration.Tags = options.Tags
	registration.Meta = options.Meta
	registration.Address = options.Address

	var err error
	registration.Check, err = newCheck(*registration, options)
	if err != nil {
		return nil, err
	}

	return registration, nil
}
